#!/usr/local/bin/python
"""
Automaton
Scriptable host interaction client
Copyright (c) 2010 Sam Saint-Pettersen

Released under the MIT License.
"""
import sys
import io
import os
import getopt
import socket
import signal
import json
import uuid
import re

class AIS_Engine:
	"""
	Script engine to parse AIS commands in a script and 
	return native Python code for execution in script sandbox
	"""
	def __init__(self):
		#
		self.DefFile = 'def.automaton.json'
		#
		self.implemented = [
		('','~'), # Blank line, treat as comment
		('~','~'), # Tilde is a comment, do nothing
		('ECHO','self.conn.send(\'$\')'), # Send message ($) to host
		('WAIT','self.conn.recv(1024)'), # Wait for return data from host
		('ENDIF','condition'), # Terminate connection to hoston a condition; Python code block TODO
		('END', 'self.conn.close()') # Terminate the connection to host absoulutely, 
		# should occur at end of scripts
		]
		self.num_builtins = len(self.implemented) # Count built-ins before...
		
		# ... importing of defined commands and variables
		self.loadDefined()

		self.implemented = dict(self.implemented) # Convert to dictionary
		
	def define(self, lineNo, line):
		pass

	def parse(self, lineNo, line):
		try:
			# Split line at space between command and parameter
			instr = re.split('\s', line, 1) 
			# Look up internal command for AIS command
			command = self.implemented[instr[0].upper()]
			# Parameter will be used with the trailing newline char removed
			param = instr[1].strip('\n')

			# Return iternal command and its parameter
			return command.replace('$', param)

		# When an invalid comamnd is encountered, throw exception
		except KeyError:
			print('Script Error: Invalid command:')
			print('\t\t[Line {lineNo}: {line}]'
			.format(lineNo=lineNo, line=line.strip('\n')))
			sys.exit(1)
		
	def loadDefined(self):
		"""
		Load user defined commands and variables from file
		"""
		pass
		#defined = json.load(io.open(self.DefFile, 'r'))
		#for el in defined: self.implemented.add(el)
		
class ScriptSandbox:
	"""
	Script sandbox to execute a script locally
	to interact with connected host
	"""
	def __init__(self, conn, debug):
		"""
		Initialization method for script thread
		"""
		self.conn = conn
		self.debug = debug

	def execute(self, script, lineNo=1):
		print('Executing \'{script}\'...\n'.format(script=script))

		# Read script file line-by-line, parse each command in file
		engine = AIS_Engine()
		scriptFile = io.open(script, 'r')
		for line in scriptFile:
			# Ignore host and port number configuration if present
			if line.startswith('!'): pass
			else:
				define = engine.define(lineNo, line)
				command = engine.parse(lineNo, line)
				if not command == '~': 
					data = eval(command)
					if self.debug: print('Raw data from host: {data}'.format(data=data))
					del data
			lineNo += 1

		scriptFile.close()
		print('Done with script; client terminated.')
		sys.exit(0)

class Connection:
	"""
	Connection class to connect to target host
	(One and only instance)
	"""
	def __init__(self, debug, host, port, script):
		"""
		Initialization method for connection class
		"""
		self.debug = debug
		self.host = host
		self.port = port
		self.script = script
		self.connectToHost()

	def connectToHost(self):
		"""
		Connect to target host and queue and pass on script to script loader
		"""
		try:
			print('\nConnecting to {host}:{port}...'.format(host=self.host, port=self.port))
			s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
			s.connect((self.host, self.port))
	
			sandbox = ScriptSandbox(s, self.debug)
			sandbox.execute(self.script)

			s.close()

		except socket.error:
			print('Connection Error: Could not connect to host.')
			sys.exit(1)

class Automaton:
	"""
	Automation instance class
	(One and only instance)
	"""
	def __init__(self):
		"""
		Initialization method for Automation
		"""
		# Application information
		self.Name = 'Automaton'
		self.Vers = '1.0'
		#
		self.ConfFile = 'conf.automaton.json'
		self.termSig = False # Termination control variable
		self.paramsOnCLI = False # Paramaters -h and -p when specified makes True

		# Configuration to use:
		#  x signature (Default: autogenerated uuid)
		# -h hostname (Default: 'localhost')
		# -p port num.(Default: 8282)
		# -d use debug (Default: False)
		self.config = {
		'x':'{signature}'.format(signature=uuid.uuid4()), 
		'-h':'localhost', '-p':8282, '-d':False
		}

		methods = {
		'-i':'displayCmdLineOps()', '-v':'displayInfo()',
		'-w':'writeConfig()'
		}

		# Allow configuration file to overwrite defaults
		self.loadConfig()

		# Add -s (script) option
		self.config['-s'] = '~'

		# Handle command line options
		try:
			opts, args = getopt.getopt(sys.argv[1:],'ivbcwdh:p:s:')
			for o, a in opts:
				if a != '': self.config[o] = a
				elif o != '-d': eval('self.{method}'.format(method=methods[o]))
				# Check if -h and/or -p have been specified on the command line;
				# if so, give them precedence over the specifications in the script file
				elif o == '-h' or o == '-p': self.paramsOnCLI = True
				# Check if -d option is specified and/or set to True, then apply debugging output
				elif o == '-d' or self.config['-d']: self.config['-d'] = True
				
			# Check script for !host:port specification string on first line in script file
			# and if so set as file when -h and/or -p have not been specified on the command line
			self.setHostFromFile(self.config['-s'])

		# Handle invalid command line options
		except getopt.GetoptError, err:
			err = str(err).capitalize()
			print('\nUsage Error: {err}.'.format(err=err))
			self.displayCmdLineOps()

		# Handle invalid script parameter
		except IOError:
			if not self.config['-s'] == '~': 
				print('\nI/O Error: Script \'{script}\' could not be loaded.'.format(script=self.config['-s']))
				# Check script file exists
				if not os.path.exists(self.config['-s']):
					print('File does not exist.')
				sys.exit(1)
			else: pass

		# Handle absence of command line options
		if len(sys.argv) == 1:
			print('\nUsage Error: No options or arguments provided.')
			self.displayCmdLineOps()

		# Handle port not being an unsigned integer
		if not self.validatePort(self.config['-p']):
			print('\nUsage Error: Port must be an unsigned integer, not \'{0}\'.'.format(self.config['-p']))
			self.displayCmdLineOps()

		print(__doc__)
		print('Hold Ctrl-C to terminate.')
		Connection(self.config['-d'], self.config['-h'], int(self.config['-p']), self.config['-s'])
		while not self.termSig:
			signal.signal(signal.SIGINT, self.quit)
			if self.termSig: sys.exit(0)

	def displayCmdLineOps(self):
		"""
		Display command line options
		"""
		print(__doc__)
		print('Usage: {program} [-i|-v|-b|-c|-w]'.format(program=sys.argv[0]))
		print('[-d -h <hostname> -p <port number>] -s <script>\n')
		print('-i: Display this information.')
		print('-v: Display version information and signature.')
		print('-b: Display built-in commands.')
		print('-c: Display defined commands and variables.')
		print('-w: (Re)write current configuration to file.')
		print('-d: Display debug information while running.')
		print('-h: Hostname to connect to. (Default: As script or {host})'
		.format(host=self.config['-h']))
		print('-p: Listen on specified port number. (Default: As script or {port})'
		.format(port=self.config['-p']))
		print('-s: Script to execute; *.ais file.\n')
		sys.exit(0)

	def	displayInfo(self):
		print('\n{app} {ver} ({plat}/{os})'
		.format(app=self.Name, ver=self.Vers, plat=sys.platform, os=os.name))
		print('\nSecurity signature:\n\n{sig}\n'.format(sig=self.config['x']))
		sys.exit(0)

	def setHostFromFile(self, script):
		scriptFile = io.open(script, 'r')
		fl = scriptFile.readline()
		scriptFile.close()
		if fl.startswith('!') and self.paramsOnCLI == False:
			conf = fl[1:].split(':')
			self.config['-h'] = conf[0]
			self.config['-p'] = int(conf[1])

			if not self.validatePort(conf[1]):
				print self.validatePort(conf[1])
				print('\nScript Error: Port specified in script was not an unsigned integer.')
				print('\t[Line 1: {line1}]'.format(line1=fl.strip('\n')))
				sys.exit(0)

	def validatePort(self, port):
		port = str(port).strip('\n')
		if port.isdigit() and int(port) > 0:
			return True
		else:
			return False
			
	def loadConfig(self):
		try:
			self.config = json.load(open(self.ConfFile, 'r'))

		except IOError:
			# Write configuration file when not found
			self.writeConfig()
			
	def writeConfig(self):
		json.dump(self.config, open(self.ConfFile, 'w'))
		print('\nInfo: Wrote configuration file...')

	def quit(self, signum, frame):
		print('\nClient terminated by user.\n')
		self.termSig = True

if __name__ == '__main__': Automaton()
